﻿//   AppStorageServiceApplicationProxy.cs
//   Copyright (c) 2012 Eswar Prakash
// 
//    This library is free software; you can redistribute it and/or modify  
//    it  under the terms of the GNU Lesser General Public License version  
//    2.1 as published by the Free Software Foundation.                     
//                                                                          
//    This library is distributed in the hope that it will be useful, but   
//    WITHOUT ANY WARRANTY; without even the implied warranty of            
//    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU     
//    Lesser General Public License for more details.                       
//                                                                          
//    You should have received a copy of the GNU Lesser General Public      
//    License along with this library; if not, write to the Free Software   
//    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  
//    USA
namespace CKSAS.AppStorage.SharedService
{
    using System;
    using Microsoft.SharePoint.Administration;
    using System.Runtime.InteropServices;
    using Microsoft.SharePoint.Utilities;
    using Microsoft.SharePoint;
    using System.ServiceModel;
    using System.ServiceModel.Channels;
    using System.ServiceModel.Configuration;
    using System.Configuration;

    /// <summary>
    /// Represents the application storage service application proxy
    /// </summary>
    [Guid("5319d40f-9aa1-4548-851f-f0c608c403db")]
    [IisWebServiceApplicationProxyBackupBehavior]
    internal sealed class AppStorageServiceApplicationProxy : SPIisWebServiceApplicationProxy, IAppStorageService
    {
        /// <summary>
        /// The load balancer to use to service the application
        /// </summary>
        [Persisted]
        private SPServiceLoadBalancer loadBalancer;

        /// <summary>
        /// The service application associated with the proxy
        /// </summary>
        [Persisted]
        private AppStorageServiceApplication application;

        /// <summary>
        /// object used to create lock for creating channel factory
        /// </summary>
        private static object channelFactoryLock = new object();

        /// <summary>
        /// the channel factory used to create service channels
        /// </summary>
        private static ChannelFactory<IAppStorageService> channelFactory = null;

        /// <summary>
        /// Private constant variable to hold the display type name of the application proxy
        /// </summary>
        private const string PROXY_TYPENAME = "Application Storage Service Application Proxy";

        /// <summary>
        /// The delegate method signature to use to execute code on the channel
        /// </summary>
        /// <typeparam name="T">The return type of the service method call</typeparam>
        /// <param name="serviceChannel">The service channel to execute the method on.</param>
        /// <returns></returns>
        private delegate T WebServiceCall<T>(IAppStorageService serviceChannel);

        /// <summary>
        /// Initializes a new instance of the <see cref="AppStorageServiceApplicationProxy"/> class.
        /// </summary>
        public AppStorageServiceApplicationProxy()
            : base()
        {
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="AppStorageServiceApplicationProxy"/> class.
        /// </summary>
        /// <param name="name">The name.</param>
        /// <param name="serviceProxy">The service proxy.</param>
        /// <param name="serviceEndPointUri">The service end point URI.</param>
        public AppStorageServiceApplicationProxy(string name, SPIisWebServiceProxy serviceProxy, Uri serviceEndPointUri)
            : base(name, serviceProxy, serviceEndPointUri)
        {
            this.loadBalancer = new SPRoundRobinServiceLoadBalancer(serviceEndPointUri);
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="AppStorageServiceApplicationProxy"/> class.
        /// </summary>
        /// <param name="name">The name.</param>
        /// <param name="serviceProxy">The service proxy.</param>
        /// <param name="serviceApplication">The service application.</param>
        internal AppStorageServiceApplicationProxy(string name, AppStorageServiceProxy serviceProxy,
                                                   AppStorageServiceApplication serviceApplication)
            : base(name, serviceProxy, serviceApplication.Uri)
        {
            this.application = serviceApplication;
            this.loadBalancer = new SPRoundRobinServiceLoadBalancer(serviceApplication.Uri);
        }

        /// <summary>
        /// Gets the display name that describes the object type in the administrative user interface.
        /// </summary>
        /// <returns>A string that contains the name of the object type.</returns>
        public override string TypeName
        {
            get { return PROXY_TYPENAME; }
        }

        /// <summary>
        /// Provisions this instance.
        /// </summary>
        public override void Provision()
        {
            this.loadBalancer.Provision();
            base.Provision();
        }

        /// <summary>
        /// </summary>
        /// <param name="deleteData"></param>
        public override void Unprovision(bool deleteData)
        {
            this.loadBalancer.Unprovision();
            base.Unprovision(deleteData);
        }

        /// <summary>
        /// Gets the load balancer.
        /// </summary>
        internal SPServiceLoadBalancer LoadBalancer
        {
            get { return this.loadBalancer; }
        }

        /// <summary>
        /// Gets the configuration.
        /// </summary>
        internal Configuration Configuration
        {
            get { return OpenClientConfiguration(SPUtility.GetGenericSetupPath(@"WebClients\AppStorage")); }
        }

        /// <summary>
        /// Gets the configured service applicaction proxy for the given service context
        /// </summary>
        /// <param name="context">The context.</param>
        /// <returns></returns>
        public static IAppStorageService GetProxy(SPServiceContext context)
        {
            if (context == null)
            {
                throw new ArgumentNullException("context");
            }

            var defaultProxy =
                (AppStorageServiceApplicationProxy)context.GetDefaultProxy(typeof(AppStorageServiceApplicationProxy));
            if (defaultProxy == null)
            {
                throw new SPException(
                    "Could not get the default application proxy for an Application Storage Service application in the provided service context");
            }

            return defaultProxy;
        }

        /// <summary>
        /// Gets the channel to the service application that should be used to execute
        /// the service call on
        /// </summary>
        /// <param name="endPointAddress">The end point address.</param>
        /// <param name="runAsProcess">if set to <c>true</c> [run as process].</param>
        /// <returns></returns>
        private IAppStorageService GetChannel(Uri endPointAddress, bool runAsProcess)
        {
            if (endPointAddress == null)
            {
                throw new ArgumentNullException("endPointAddress");
            }

            if (channelFactory == null)
            {
                lock (channelFactoryLock)
                {
                    if (channelFactory == null)
                    {
                        channelFactory = new ConfigurationChannelFactory<IAppStorageService>(endPointAddress.Scheme,
                                                                                             this.Configuration,
                                                                                             new EndpointAddress(
                                                                                                 endPointAddress));
                        channelFactory.ConfigureCredentials(SPServiceAuthenticationMode.Claims);
                    }
                }
            }

            // do we need to run this call as a process identity?
            if (runAsProcess)
            {
                return channelFactory.CreateChannelAsProcess(new EndpointAddress(endPointAddress));
            }
            else
            {
                return channelFactory.CreateChannelActingAsLoggedOnUser(new EndpointAddress(endPointAddress));
            }
        }

        /// <summary>
        /// Executes the code block on service channel
        /// </summary>
        /// <typeparam name="T">Type of return value expected</typeparam>
        /// <param name="operation">The operation to execute.</param>
        /// <param name="codeBlock">The code block to execute on the channel.</param>
        /// <param name="runAsProcess">if set to <c>true</c> runs the service call as the process identity.</param>
        /// <returns></returns>
        private T ExecuteOnChannel<T>(string operation, WebServiceCall<T> codeBlock, bool runAsProcess)
        {
            if (codeBlock == null)
            {
                throw new ArgumentNullException("codeBlock");
            }


            // get the load balancer
            var serviceLoadBalancer = this.LoadBalancer;
            if (serviceLoadBalancer == null)
            {
                throw new InvalidOperationException("Proxy load balancer was not found");
            }

            // get a new load balancer context for operation
            var loadBalancerContext = serviceLoadBalancer.BeginOperation();
            T result;

            IChannel channel = null;

            try
            {
                using (new SPMonitoredScope(operation))
                {
                    // now execute the code block
                    channel = (IChannel)this.GetChannel(loadBalancerContext.EndpointAddress, runAsProcess);
                    var serviceChannel = (IAppStorageService)channel;

                    try
                    {
                        result = codeBlock(serviceChannel);

                        // close the channel
                        channel.Close();
                    }
                    finally
                    {
                        if (channel.State != CommunicationState.Closed)
                        {
                            channel.Abort();
                        }
                    }
                }
            }
            catch (EndpointNotFoundException)
            {
                loadBalancerContext.Status = SPServiceLoadBalancerStatus.Failed;
                throw;
            }
            finally
            {
                if (channel != null)
                {
                    if (channel.State != CommunicationState.Closed)
                    {
                        channel.Abort();
                    }
                }

                loadBalancerContext.EndOperation();
            }

            return result;
        }

        /// <summary>
        /// Gets a stored object given its key in the storage.
        /// </summary>
        /// <param name="partition">The partition for which the storage item needs to be returned.</param>
        /// <param name="name">The unique name of the storage item in the storage.</param>
        /// <returns>
        /// An instance of <see cref="StorageItem"/> containing information about the storage item
        /// </returns>
        StorageItem IAppStorageService.GetStorageItem(StoragePartition partition, string name)
        {
            return this.ExecuteOnChannel("GetStorageItem", (x) => x.GetStorageItem(partition, name), false);
        }

        /// <summary>
        /// Adds the stored item to the underlying storage
        /// </summary>
        /// <param name="partition">The partition for which the storage item is indeed.</param>
        /// <param name="name">The unique name of the item in the storage.</param>
        /// <param name="item">The item information that needs to be stored.</param>
        /// <returns>
        /// An instance of <see cref="StorageItem" /> containing the newly created storage item details
        /// </returns>
        StorageItem IAppStorageService.AddStorageItem(StoragePartition partition, string name, StorageItem item)
        {
            return this.ExecuteOnChannel("AddStorageItem", (x) => x.AddStorageItem(partition, name, item), false);
        }

        /// <summary>
        /// Updates the stored item in the underlying storage
        /// </summary>
        /// <param name="partition">The partition for which the storage item is indeed.</param>
        /// <param name="name">The unique name of the item in the storage.</param>
        /// <param name="item">The item information that needs to be stored.</param>
        /// <returns>
        /// An instance of <see cref="StorageItem" /> containing the newly created storage item details
        /// </returns>
        StorageItem IAppStorageService.UpdateStorageItem(StoragePartition partition, string name, StorageItem item)
        {
            return this.ExecuteOnChannel("AddStorageItem", (x) => x.UpdateStorageItem(partition, name, item), false);
        }

        /// <summary>
        /// Deletes the storage item from the underlying storage
        /// </summary>
        /// <param name="partition">The partition where the storage item exists.</param>
        /// <param name="name">The name of the storage item in the storage.</param>
        void IAppStorageService.DeleteStorageItem(StoragePartition partition, string name)
        {
            this.ExecuteOnChannel("AddStorageItem", (x) =>
            {
                x.DeleteStorageItem(partition, name);
                return -1;
            }, false);
        }
    }
}